算法复杂度
时间复杂度和空间复杂度是衡量一个算法效率的重要标准。
基本操作数
同一个算法在不同的计算机上运行的速度会有一定的差别,并且实际运行速度难以在理论上进行计算,实际去测量又比较麻烦,所以我们通常考虑的不是算法运行的实际用时,而是算法运行所需要进行的基本操作的数量。
在普通的计算机上,加减乘除、访问变量(基本数据类型的变量,下同)、给变量赋值等都可以看作基本操作。
对基本操作的计数或是估测可以作为评判算法用时的指标。
时间复杂度
定义
衡量一个算法的快慢,一定要考虑数据规模的大小。所谓数据规模,一般指输入的数字个数、输入中给出的图的点数与边数等等。一般来说,数据规模越大,算法的用时就越长。而在算法竞赛中,我们衡量一个算法的效率时,最重要的不是看它在某个数据规 模下的用时,而是看它的用时随数据规模而增长的趋势,即 时间复杂度。
引入
考虑用时随数据规模变化的趋势的主要原因有以下几点:
- 现代计算机每秒可以处理数亿乃至更多次基本运算,因此我们处理的数据规模通常很大。如果算法 A 在规模为 的数据上用时为 而算法 B 在规模为 的数据上用时为 ,在数据规模小于 时算法 B 用时更短,但在一秒钟内算法 A 可以处理数百万规模的数据,而算法 B 只能处理数万规模的数据。在允许算法执行时间更久时,时间复杂度对可处理数据规模的影响就会更加明显,远大于同一数据规模下用时的影响。
- 我们采用基本操作数来表示算法的用时,而不同的基本操作实际用时是不同的,例如加减法的用时远小于除法的用时。计算时间复杂度而忽略不同基本操作之间的区别以及一次基本操作与十次基本操作之间的区别,可以消除基本操作间用时不同的影响。
当然,算法的运行用时并非完全由输入规模决定,而是也与输入的内容相关。所以,时间复杂度又分为几种,例如:
- 最坏时间复杂度,即每个输入规模下用时最长的输入对应的时间复杂度。在算法竞赛中,由于输入可以在给定的数据范围内任意给定,我们为保证算法能够通过某个数据范围内的任何数据,一般考 虑最坏时间复杂度。
- 平均(期望)时间复杂度,即每个输入规模下所有可能输入对应用时的平均值的复杂度(随机输入下期望用时的复杂度)。
所谓「用时随数据规模而增长的趋势」是一个模糊的概念,我们需要借助下文所介绍的 渐进符号 来形式化地表示时间复杂度。
渐进符号的定义
渐进符号是函数的阶的规范描述。简单来说,渐进符号忽略了一个函数中增长较慢的部分以及各项的系数(在时间复杂度相关分析中,系数一般被称作「常数」),而保留了可以用来表明该函数增长趋势的重要部分。
一个简单的记忆方法是,含等于(非严格)用大写,不含等于(严格)用小写,相等是 ,小于是 ,大于是 。大 和小 原本是希腊字母 Omicron,由于字形相同,也可以理解为拉丁字母的大 和小 。
在英文中,词根「-micro-」 和「-mega-」常用于表示 10 的负六次方(百万分之一)和六次方(百万),也表示「小」和「大」。小和大也是希腊字母 Omicron 和 Omega 常表示的含义。
大 Θ 符号
对于函数 和 ,,当且仅当 ,使得 。
也就是说 ,如果函数 ,那么我们能找到两个正数 使得 被 和 夹在中间。
例如,, 这里的 可以分别是 。,这里的 可以分别是 。
大 O 符号
符号同时给了我们一个函数的上下界,如果只知道一个函数的渐进上界而不知道其渐进下界,可以使用 符号。,当且仅当 ,使得 。
研究时间复杂度时通常会使用 符号,因为我们关注的通常是程序用时的上界,而不关心其用时的下界。
需要注意的是,这里的「上界」和「下界」是对于函数的变化趋势而言的,而不是对算法而言的。算法用时的上界对应的是「最坏时间复杂度」而非大 记号。所以,使用 记号表示最坏时间复杂度是完全可行的,甚至可以说 比 更加精确,而使用 记号的主要原因,一是我们有时只能证明时间复杂度的上界而无法证明其下界(这种情况一般出现在较为复杂的算法以及复杂度分析),二是 在电脑上输入更方便一些。
大 Ω 符号
同样的,我们使用 符号来描述一个函数的渐进下界。,当且仅当 ,使得 。
小 o 符号
如果说 符号相当于小于等于号,那么 符号就相当于小于号。
小 符号大量应用于数学分析中,函数在某点处的泰勒展开式拥有皮亚诺余项,使用小 符号表示严格小于,从 而进行等价无穷小的渐进分析。
,当且仅当对于任意给定的正数 ,,使得 。
小 ω 符号
如果说 符号相当于大 于等于号,那么 符号就相当于大于号。
,当且仅当对于任意给定的正数 ,,使得 。